package se.webbzon.boltzmann.game.object.player;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import javax.media.j3d.Transform3D;
import javax.vecmath.Vector3d;

import se.webbzon.boltzmann.game.character.AbstractCharacter;
import se.webbzon.boltzmann.game.character.CharacterActionEnum;
import se.webbzon.boltzmann.game.character.CharacterException;
import se.webbzon.boltzmann.game.object.platform.Platform;
import se.webbzon.boltzmann.input.Input;
import se.webbzon.boltzmann.input.Button;
import se.webbzon.boltzmann.input.InputListener;
import se.webbzon.boltzmann.player.Player;
import se.webbzon.boltzmann.player.PlayerCharacter;
import se.webbzon.boltzmann.sound.SoundUtils;
import se.webbzon.oschi01.collision3d.CollisionCube3D;
import se.webbzon.oschi01.collision3d.CollisionNode3D;
import se.webbzon.oschi01.utilities.ArrayUtils;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldobject.WorldObject;
import se.webbzon.oschi01.worldobject.WorldStaticObject;

public abstract class AbstractPlayerCharacter extends AbstractCharacter {
	
	// The down hill coefficient used to move the player
	// upwards and downwards a hill
	private static final double dh = Math.sin(Math.PI/8);
	
	// The player associated with this character
	private final Player player;
	
	// The input listener associated with this character
	private final InputListener inputListener;
	
	// The shadow of the character
	private final AbstractPlayerCharacterShadow shadow;
	
	// A set of all the character blockers
	private final Set<Object> blockers;
	
	// Whatever this character is blocked
	private Boolean blocked;
	
	// Whatever the jump button has been pressed
	private volatile boolean jump;
	
	// The motion direction and speed of the character
	private volatile double motionDirection, motionSpeed;
	
	// Whatever the special button has been pressed
	private volatile boolean special;
	
	// The cooldown until next possible air control (in number of steps)
	private int airCooldown;
	
	// The focus gathered by the player
	private double focus; 
	
	// A timer used to keep track on whatever the player is invulnerable or not
	private int invulnerableTimer;
	
	// A timer used to create footsteps
	private int footstepTimer;
	
	// A blocker used when the player is talking
	private Object talkerBlocker;
	
	// The platform on which the player is standing
	private Platform platform;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new abstract player character from a character name. **/
	public AbstractPlayerCharacter(	Player player, 
									PlayerCharacter character) 
			throws CharacterException {
		super(	WorldObject.onCreate + WorldObject.onStep + WorldObject.onDestroy,
				player.getCharacter().getCharacterTextureSet());
		if (player.getCharacter() != character)
				throw new CharacterException(	"The character of the provided " +
												"player is not equal to the " +
												"provided character player " + 
												"character!");
		
		this.player = player;
		blockers = new HashSet<Object>();
		blocked = false;
		shadow = new AbstractPlayerCharacterShadow();
		inputListener = new InputListenerImpl();
		
		CollisionNode3D mask = new CollisionCube3D();
		Transform3D maskTransform = new Transform3D();
		maskTransform.setScale(new Vector3d(0.25,1,0.25));
		//maskTransform.setTranslation(new Vector3d(0,0,0));
		mask.transform(maskTransform);
		setMask(mask);
		
		setSize(32,32,32);
		setAction(CharacterActionEnum.WALK);
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Creates a new abstract player character by using the given player. **/
	public static AbstractPlayerCharacter makePlayerCharacter(Player player) 
			throws CharacterException {
		return player.getCharacter().makePlayerCharacter(player);
	}
	
	/** Returns the number of steps between footsteps of this character. 
	 * If -1 is returned then the character is assumed to not have any 
	 * footsteps. **/
	public abstract int getFootstepCooldown();
	
	/** Returns the jump speed of this character. **/
	public abstract double getJumpSpeed();
	
	/** Returns the ground speed of this character. **/
	public abstract double getGroundSpeed();
	
	/** Returns the air speed of this character. **/
	public abstract double getAirSpeed();
	
	/** Returns the water speed of this character. **/
	public abstract double getWaterSpeed();
	
	/** Returns the focus speed of this character. **/
	public abstract double getFocusSpeed();
	
	/** This method is invoked when the character starts to focus. **/
	public abstract void onFocusBegin();
	
	/** This method is invoked when the character stops to focus. **/
	public abstract void onFocusEnd();
	
	/** Returns the focus gathered by the player. **/
	public double getFocus() {
		return focus;
	}
	
	/** Returns true if the player is invulnerable. **/
	public boolean isInvulnerable() {
		return invulnerableTimer > 0;
	}
	
	/** Turns the player invulnerable for a given amount of steps. **/
	public void setInvulnerable(int numberOfSteps) {
		invulnerableTimer = numberOfSteps > 0 ? numberOfSteps : 0;
	}
	
	/** Returns the player associated with this character. **/
	public Player getPlayer() {
		return player;
	}
	
	/** Returns a synchronized boolean which indicated
	 * whatever the character is blocked. **/
	public Boolean isBlocked() {
		return blocked;
	}
	
	/** Adds and returns a new blocker for this player character. **/
	public Object containBlocker() {
		synchronized (blocked) {
			Object blocker = new Object();
			blockers.add(blocker);
			blocked = true;
			return blocker;
		}
	}
	
	/** Releases a blocker for this player character. **/
	public synchronized void releaseBlocker(Object blocker) {
		synchronized (blocked) {
			blockers.remove(blocker);
			if (blockers.size() == 0)
				blocked = false;
		}
	}
	
	/** Returns true if the player action was set to the given
	 * talking value (true = talking, false = not talking). **/
	public synchronized boolean setTalking(boolean talk) {
		if (talk && talkerBlocker == null) {
			talkerBlocker = containBlocker();
			setAction(CharacterActionEnum.TALK);
			setActionSpeed(0.3);
			setMotion(0,0);
			return true;
		} else if (!talk && talkerBlocker != null) {
			releaseBlocker(talkerBlocker);
			talkerBlocker = null;
			setAction(CharacterActionEnum.STAND);
			return true;
		} else
			return false;
	}
	
	public synchronized boolean isTalking() {
		return talkerBlocker != null;
	}
	
	/** Makes this character jump. **/
	public void jump() {
		synchronized (blocked) {
			jump = true;
		}
	}
	
	/** Sets the motion for this character. **/
	public void setMotion(double motionDirection, double motionSpeed) {
		synchronized (blocked) {
			if (motionDirection < 0)
				this.motionDirection = motionDirection + 2 * Math.PI;
			else
				this.motionDirection = motionDirection;
			this.motionSpeed = Math.min(1, Math.max(motionSpeed, 0));
		}
	}
	
	@Override public void onCreate(WorldArea world) {
		player.getInput().addListener(inputListener);
		world.createInstance(shadow);
	}
	
	@Override public void onStep(WorldArea world) {
		
		// Turn on solidity
		setSolid(true);
		
		/*------------------------------------------------------------
		Vertical collision checking
		------------------------------------------------------------*/
		
		// Perform vertical collision check and set the
		// motion system to null (platform = null)
		final Vector3d translation = getTranslation();
		final double yStash = translation.y;
		final double yVelocity;
		if (platform == null)
			yVelocity = getYVelocity();
		else
			yVelocity = getYVelocity() - platform.getYVelocity();
		translation.y += yVelocity-1;
		final Collection<WorldStaticObject> vInstances = world.placeMeetingAll(this,translation,null,null);
		final boolean vFree;
		if (vInstances.isEmpty()) {
			platform = null;
			vFree = true;
		} else
			vFree = false;
		
		/*------------------------------------------------------------
		Simple motion
		------------------------------------------------------------*/
		
		// Set the velocity of the player
		if (!special) {
			if (!vFree)
				setVelocity(Math.cos(motionDirection) * motionSpeed * getGroundSpeed(), 
							getYVelocity(), 
							-Math.sin(motionDirection) * motionSpeed * getGroundSpeed());
			else if (airCooldown == 0) {
				airCooldown = 5;
				setVelocity(getXVelocity() + Math.cos(motionDirection) * motionSpeed * getAirSpeed(), 
							getYVelocity(), 
							getZVelocity() - Math.sin(motionDirection) * motionSpeed * getAirSpeed());
			} else if (airCooldown > 0)
				airCooldown--;
		} else
			setVelocity(0,getYVelocity(),0);
		
		// Update the velocity of the player relative to the system
		// in which the player is standing
		if (vFree) {
			// Accelerate if no vertical collision occurs
			setAcceleration(getXAcceleration(),-0.5,getZAcceleration());
			
		} else {
			// Turn of gravity during ground contact
			setAcceleration(getXAcceleration(),0,getZAcceleration());
			
			// Find a motion system and move with it
			platform = findPlatform(vInstances);
			if (platform == null)
				setVelocity(getXVelocity(), 0, getZVelocity());
			else
				setVelocity(getXVelocity() + platform.getXVelocity(), 
							platform.getYVelocity(), 
							getZVelocity() + platform.getZVelocity());
			
			// Touch the ground/ceiling
			verticalTouch(yVelocity);
		}
		
		// Jump if the player perform a jump action
		if (jump && !special) {
			jump = false;
			performJump(vFree);
		}
		
		/*------------------------------------------------------------
		Update effects and animations
		------------------------------------------------------------*/
		
		// Update the animation of the player
		if (!isTalking()) {
			if (vFree)
				animteFall();
			else if (!special)
				animateMotion(motionSpeed);
		}
			
		// Move the shadow with the player
		if (vFree || getYVelocity() >= getJumpSpeed())
			shadow.setPosition(getX(), shadow.getY(), getZ());
		else
			shadow.setPosition(	getX() + getXVelocity(), 
								getY() + getYVelocity() - 16, 
								getZ() + getZVelocity());
		
		// Update the footstep counter
		if (motionSpeed > 0 && footstepTimer >= 0) {
			if (footstepTimer > 0)
				footstepTimer--;
			else if (!vFree) {
				SoundUtils.playSound(ArrayUtils.random("footstep_01","footstep_02","footstep_03"));
				footstepTimer = getFootstepCooldown();
			}
		} else if (footstepTimer < getFootstepCooldown())
			footstepTimer = getFootstepCooldown();
		
		/*------------------------------------------------------------
		Horizontal motion/collision checking
		------------------------------------------------------------*/
	
		// Check if the player is in motion
		if (getXVelocity() != 0 || getZVelocity() != 0) {
			
			// Check if the character has a free horizontal path
			translation.y = yStash;
			translation.x += getXVelocity();
			translation.z += getZVelocity();
			final boolean hFree = world.placeFree(this,translation,null,null);
			final double dy = Math.hypot(getXVelocity(), getZVelocity());
			
			if (!hFree) {
				// Move up along a hill side
				translation.y += dy;
				final boolean upFree = world.placeFree(this,translation,null,null);
				if (!upFree)
					setVelocity(0, getYVelocity(), 0);
				else {
					setPosition(getX(),getY() + dy,getZ());
					world.moveContact(this, new Vector3d(0,-1,0), 8);
				}
				
			} else if (!vFree) {
				// Move down along a hill side
				final double dhdy = dh * dy;
				translation.y -= dhdy;
				final boolean downFree = world.placeFree(this,translation,null,null);
				if (downFree) {
					setPosition(getX(),getY() - dhdy,getZ());
					world.moveContact(this, new Vector3d(0,-1,0), 8);
				}
			}
		}
		
		/*------------------------------------------------------------
		Other functionality
		------------------------------------------------------------*/
		
		// Turn on and off visibility upon invulnerability
		dischargeInvulnerability(1);

		// React on input for special attack and motion
		if (special) {
			setActionSpeed(0);
			chargeSpecial(getFocusSpeed());
		} else if (!isTalking()) {
			setActionSpeed(motionSpeed*0.5);
			releaseSpecial();
		}
		
		// Turn of solidity
		//refreshTransform(true, true);
		setSolid(false);
	}
	
	@Override public void onDestroy(WorldArea world) {
		player.getInput().removeListener(inputListener);
		world.destroyInstance(shadow);
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Charges the special attack with the given amount. **/
	private final void chargeSpecial(double amount) {
		if (focus == 0)
			onFocusBegin();
		focus = Math.min(1, focus + amount);
	}
	
	/** Releases the special attack. **/
	private final void releaseSpecial() {
		if (focus > 0) {
			onFocusEnd();
			focus = 0;
		}
	}
	
	/** Discharges the invulnerability by the the given amount. During
	 * discharge the players visibility will be oscillating. **/
	private final void dischargeInvulnerability(int discharge) {
		if (invulnerableTimer > 0) {
			invulnerableTimer = Math.max(0, invulnerableTimer - discharge);
			if (invulnerableTimer % 10 < 5)
				setVisible(true);
			else
				setVisible(false);
		}
	}
	
	/** Finds and returns the first available platform below or above the 
	 * player character (depending on the provided collection).
	 * If no platform is found then null is returned. **/
	private final Platform findPlatform(Collection<WorldStaticObject> vInstances) {
		for (WorldStaticObject object : vInstances) {
			if (object instanceof Platform)
				return (Platform) object;
		}
		return null;
	}
	
	/** Touches the ground/platform or ceiling depending on the
	 * provided vertical velocity of the player character. The
	 * provided vertical velocity is relative to the current
	 * system in which the player character stands.**/
	private final void verticalTouch(double yVelocity) {
		if (yVelocity < 0)
			getWorld().moveContact(this, new Vector3d(0,-1,0), 8);
		else if (yVelocity > 0 && platform == null)
			getWorld().moveContact(this, new Vector3d(0,1,0), 8);
	}
	
	/** Sets the falling animation for this player character. **/
	private final void animteFall() {
		setAction(CharacterActionEnum.JUMP);
	}
	
	/** Sets the motion animation for this character**/
	private final void animateMotion(double motionSpeed) {
		if (motionSpeed > 0) {
			setFacing(motionDirection);
			setAction(CharacterActionEnum.WALK);
		} else
			setAction(CharacterActionEnum.STAND);
	}
	
	/** Performs a jump with the player character is the 
	 * there is a ground below (that is if vFree is false). **/
	private final void performJump(boolean vFree) {
		if (!vFree) {
			setVelocity(getXVelocity(), 
						getYVelocity() + getJumpSpeed(), 
						getZVelocity());
			SoundUtils.playSound("player_jump");
		}
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	private final class InputListenerImpl implements InputListener {
		
		@Override public void primaryAxisChanged(Input input, double direction, double value) {
			synchronized (blocked) {
				if (!isBlocked()) {
					motionDirection = direction;
					motionSpeed = value;
				}
			}
		}

		@Override public void secondaryAxisChanged(Input input, double direction, double value) {};

		@Override public void buttonPressed(Input input, Button button) {
			synchronized (blocked) {
				if (!isBlocked()) switch (button) {
				case JUMP: jump = true; break;
				case SPECIAL: special = true; break;
				}
			}
		}

		@Override public void buttonReleased(Input input, Button button) {
			synchronized (isBlocked()) {
				if (!isBlocked()) switch (button) {
				case SPECIAL: special = false; break;
				}
			}
		}
		
		@Override public void bindingAccepted(Input input, Button button) {};
		
	}
	

}
